﻿using System.Collections.Concurrent;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using OnDash.Core;
using OnDash.Mathematics.Geometry;
using OnDash.Mathematics.Vector;
using OnDash.Windowing.Common;
using OnDash.Windowing.Common.Enums;
using OnDash.Windowing.Common.Events;
using OnDash.Windowing.Common.Inputs;
using OnDash.Windowing.Common.Inputs.Enums;
using OnDash.Windowing.Desktop.Events;
using OnDash.Windowing.GraphicsLibraryFramework;
using OnDash.Windowing.GraphicsLibraryFramework.Enums;
using OnDash.Windowing.GraphicsLibraryFramework.Inputs;
using ContextApi = OnDash.Windowing.Common.Enums.ContextApi;
using WindowPosCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.WindowPosCallback;
using WindowSizeCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.WindowSizeCallback;
using WindowIconifyCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.WindowIconifyCallback;
using WindowMaximizeCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.WindowMaximizeCallback;
using WindowFocusCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.WindowFocusCallback;
using CharCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.CharCallback;
using ScrollCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.ScrollCallback;
using WindowRefreshCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.WindowRefreshCallback;
using WindowCloseCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.WindowCloseCallback;
using KeyCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.KeyCallback;
using CursorEnterCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.CursorEnterCallback;
using MouseButtonCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.MouseButtonCallback;
using CursorPosCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.CursorPosCallback;
using DropCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.DropCallback;
using JoystickCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.JoystickCallback;
using MonitorCallback = OnDash.Windowing.GraphicsLibraryFramework.GlfwCallbacks.MonitorCallback;

namespace OnDash.Windowing.Desktop;

public class NativeWindow : IDisposable
{
    private static ConcurrentQueue<ExceptionDispatchInfo?> _callbackExceptions = new();

    private static List<ExceptionDispatchInfo?> _localThreadExceptions = [];

    private readonly JoystickState[] _joystickStates = new JoystickState[16];

    private (int numerator, int denominator)? _aspectRatio;

    private Vector2 _cachedWindowClientSize;
    private Vector2 _cachedWindowLocation;
    private CharCallback _charCallback;

    private MonitorHandle _currentMonitor;
    private CursorEnterCallback _cursorEnterCallback;
    private CursorPosCallback _cursorPosCallback;

    private bool _disposedValue;
    private DropCallback _dropCallback;

    private unsafe Cursor* _glfwCursor;

    private WindowIcon _icon;

    private bool _isFocused;

    private bool _isVisible;
    private JoystickCallback? _joystickCallback;
    private KeyCallback _keyCallback;

    private Vector2 _lastReportedMousePos;

    private Vector2 _location;

    private MouseCursor _managedCursor = MouseCursor.Default;
    private Vector2? _maximumSize;
    private Vector2? _minimumSize;
    private MonitorCallback _monitorCallback;
    private MouseButtonCallback _mouseButtonCallback;
    private WindowState _notMinimizedWindowState;
    private ScrollCallback _scrollCallback;

    private Vector2 _size;

    private string _title;

    private VSyncMode _vSync;

    private WindowBorder _windowBorder;
    private WindowCloseCallback _windowCloseCallback;
    private WindowFocusCallback _windowFocusCallback;
    private WindowIconifyCallback _windowIconifyCallback;
    private WindowMaximizeCallback _windowMaximizeCallback;

    private WindowPosCallback _windowPosCallback;
    private WindowRefreshCallback _windowRefreshCallback;
    private WindowSizeCallback _windowSizeCallback;

    private WindowState _windowState = WindowState.Normal;

    public unsafe NativeWindow(NativeWindowSettings settings)
    {
        GlfwProvider.EnsureInitialized();

        _title = settings.Title;

        _currentMonitor = settings.CurrentMonitor;

        switch (settings.WindowBorder)
        {
            case WindowBorder.Hidden:
                Glfw.WindowHint(WindowHintBool.Decorated, false);
                break;
            case WindowBorder.Resizable:
                Glfw.WindowHint(WindowHintBool.Resizable, true);
                break;
            case WindowBorder.Fixed:
                Glfw.WindowHint(WindowHintBool.Resizable, false);
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        var isOpenGl = false;
        Api = settings.Api;
        switch (settings.Api)
        {
            case ContextApi.NoApi:
                Glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.NoApi);
                break;
            case ContextApi.OpenGlEs:
                Glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlEsApi);
                isOpenGl = true;
                break;
            case ContextApi.OpenGl:
                Glfw.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi);
                isOpenGl = true;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        Glfw.WindowHint(WindowHintInt.ContextVersionMajor, settings.ApiVersion.Major);
        Glfw.WindowHint(WindowHintInt.ContextVersionMinor, settings.ApiVersion.Minor);

        ApiVersion = settings.ApiVersion;

        Flags = settings.Flags;
        if (settings.Flags.HasFlag(ContextFlags.ForwardCompatible))
        {
            Glfw.WindowHint(WindowHintBool.OpenGlForwardCompat, true);
        }

        if (settings.Flags.HasFlag(ContextFlags.Debug))
        {
            Glfw.WindowHint(WindowHintBool.OpenGlDebugContext, true);
        }

        Profile = settings.Profile;
        switch (settings.Profile)
        {
            case ContextProfile.Any:
                Glfw.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
                break;
            case ContextProfile.Compatability:
                Glfw.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Compat);
                break;
            case ContextProfile.Core:
                Glfw.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Core);
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        Glfw.WindowHint(WindowHintBool.Focused, settings.StartFocused);
        _windowBorder = settings.WindowBorder;

        _isVisible = settings.StartVisible;
        Glfw.WindowHint(WindowHintBool.Visible, _isVisible);
        Glfw.WindowHint(WindowHintInt.Samples, settings.NumberOfSamples);
        Glfw.WindowHint(WindowHintBool.SrgbCapable, settings.SrgbCapable);

        {
            var monitor = settings.CurrentMonitor.ToUnsafePtr<GraphicsLibraryFramework.Monitor>();
            var modePtr = Glfw.GetVideoMode(monitor);
            Glfw.WindowHint(WindowHintInt.RedBits, settings.RedBits ?? modePtr->RedBits);
            Glfw.WindowHint(WindowHintInt.GreenBits, settings.GreenBits ?? modePtr->GreenBits);
            Glfw.WindowHint(WindowHintInt.BlueBits, settings.BlueBits ?? modePtr->BlueBits);
            if (settings.AlphaBits.HasValue)
                Glfw.WindowHint(WindowHintInt.AlphaBits, settings.AlphaBits.Value);

            if (settings.DepthBits.HasValue)
                Glfw.WindowHint(WindowHintInt.DepthBits, settings.DepthBits.Value);

            if (settings.StencilBits.HasValue)
                Glfw.WindowHint(WindowHintInt.StencilBits, settings.StencilBits.Value);

            Glfw.WindowHint(WindowHintInt.RefreshRate, modePtr->RefreshRate);
            if (settings.WindowState == WindowState.FullScreen && _isVisible)
            {
                _windowState = WindowState.FullScreen;
                _cachedWindowLocation = settings.Location ?? new Vector2(32, 32);
                _cachedWindowClientSize = settings.Size;
                WindowPtr = Glfw.CreateWindow(modePtr->Width, modePtr->Height, _title, monitor,
                    (Window*)(settings.SharedContext?.WindowPtr ?? IntPtr.Zero));
            }
            else
            {
                WindowPtr = Glfw.CreateWindow((int)settings.Size.X, (int)settings.Size.Y, _title, null,
                    (Window*)(settings.SharedContext?.WindowPtr ?? IntPtr.Zero));
            }
        }
        if (settings.Api != ContextApi.NoApi)
            Context = new GlfwGraphicsContext(WindowPtr);

        Exists = true;

        if (isOpenGl)
        {
            Context?.MakeCurrent();
            if (settings.AutoLoadBindings) InitializeGlBindings();
        }

        Glfw.SetInputMode(WindowPtr, LockKeyModAttribute.LockKeyMods, true);

        RegisterWindowCallbacks();

        InitialiseJoystickStates();

        _isFocused = settings.StartFocused;
        if (settings.StartFocused) Focus();

        WindowState = settings.WindowState;
        IsEventDriven = settings.IsEventDriven;
        Icon = settings.Icon;

        if (settings.Location.HasValue) Location = settings.Location.Value;

        Glfw.GetWindowSize(WindowPtr, out var width, out var height);

        HandleResize(width, height);

        AspectRatio = settings.AspectRatio;
        _minimumSize = settings.MinimumSize;
        _maximumSize = settings.MaximumSize;

        Glfw.SetWindowSizeLimits(WindowPtr, (int)(_minimumSize?.X ?? Glfw.DontCare),
            (int)(_minimumSize?.Y ?? Glfw.DontCare),
            (int)(_maximumSize?.X ?? Glfw.DontCare),
            (int)(_maximumSize?.Y ?? Glfw.DontCare));

        Glfw.GetWindowPos(WindowPtr, out var x, out var y);
        _location = new Vector2(x, y);

        Glfw.GetCursorPos(WindowPtr, out var mouseX, out var mouseY);
        _lastReportedMousePos = new Vector2(mouseX, mouseY);
        MouseState.Position = _lastReportedMousePos;

        _isFocused = Glfw.GetWindowAttrib(WindowPtr, WindowAttributeGetBool.Focused);
    }

    public unsafe Window* WindowPtr { get; }
    public KeyboardState KeyboardState { get; } = new();

    [Obsolete("Use " + nameof(KeyboardState.WasKeyDown) + " instead.", true)]
    public KeyboardState LastKeyboardState => null!;

    public IReadOnlyList<JoystickState> JoystickStates =>
        _joystickStates;

    [Obsolete(
        "Use " + nameof(JoystickState.WasButtonDown) + ", " + nameof(JoystickState.GetAxisPrevious) + " and " +
        nameof(JoystickState.GetHatPrevious) + " instead.", true)]
    public IReadOnlyList<JoystickState> LastJoystickStates =>
        null!;


    public unsafe Vector2 MousePosition
    {
        get => _lastReportedMousePos;
        set => Glfw.SetCursorPos(WindowPtr, value.X, value.Y);
    }

    [Obsolete("Use " + nameof(MouseState.Delta) + " member of the " + nameof(MouseState) + " property instead.", true)]
    public Vector2 MouseDelta =>
        Vector2.Zero;

    public MouseState MouseState { get; } = new MouseState();

    [Obsolete(
        "Use " + nameof(MouseState.WasButtonDown) + " and " + nameof(MouseState.PreviousPosition) + " members of the " +
        nameof(MouseState) + " property instead.", true)]
    public MouseState LastMouseState => null!;

    public bool IsAnyKeyDown =>
        KeyboardState.IsAnyKeyDown;

    public bool IsAnyMouseButtonDown =>
        MouseState.IsAnyButtonDown;

    public VSyncMode VSync
    {
        get
        {
            if (Context == null)
                throw new InvalidOperationException("Cannot control vsync when running with ContextAPI.NoAPI.");
            return _vSync;
        }

        set
        {
            if (Context == null)
                throw new InvalidOperationException("Cannot control vsync when running with ContextAPI.NoAPI.");

            switch (value)
            {
                case VSyncMode.On:
                    Context.SwapInterval = 1;
                    break;

                case VSyncMode.Off:
                    Context.SwapInterval = 0;
                    break;
                case VSyncMode.Adaptive:
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(value), value, null);
            }

            _vSync = value;
        }
    }

    public unsafe WindowIcon Icon
    {
        get => _icon;
        set
        {
            var images = value.Images;
            Span<GCHandle> handles = stackalloc GCHandle[images.Length];
            Span<GraphicsLibraryFramework.Image> glfwImages =
                stackalloc GraphicsLibraryFramework.Image[images.Length];

            for (var i = 0; i < images.Length; i++)
            {
                var image = images[i];
                handles[i] = GCHandle.Alloc(image.Data, GCHandleType.Pinned);
                var addrOfPinnedObject = (byte*)handles[i].AddrOfPinnedObject();
                glfwImages[i] =
                    new GraphicsLibraryFramework.Image(image.Width, image.Height, addrOfPinnedObject);
            }

            Glfw.SetWindowIcon(WindowPtr, glfwImages);

            foreach (var handle in handles)
                handle.Free();

            _icon = value;
        }
    }

    public bool IsEventDriven { get; set; }

    public unsafe string ClipboardString
    {
        get => Glfw.GetClipboardString(WindowPtr);
        set => Glfw.SetClipboardString(WindowPtr, value);
    }

    public unsafe string Title
    {
        get => _title;
        set
        {
            Glfw.SetWindowTitle(WindowPtr, value);
            _title = value;
        }
    }

    public ContextApi Api { get; }

    public ContextProfile Profile { get; }

    public ContextFlags Flags { get; }

    public Version ApiVersion { get; }

    public IGlfwGraphicsContext Context { get; }

    public unsafe MonitorHandle CurrentMonitor
    {
        get => _currentMonitor;
        set
        {
            var monitor = value.ToUnsafePtr<GraphicsLibraryFramework.Monitor>();
            var mode = Glfw.GetVideoMode(monitor);
            Glfw.SetWindowMonitor(WindowPtr, monitor, (int)_location.X, (int)_location.Y,
                (int)_size.X, (int)_size.Y,
                mode->RefreshRate);

            _currentMonitor = value;
        }
    }


    public bool IsFocused =>
        _isFocused;

    public bool IsVisible
    {
        get => _isVisible;
        set
        {
            if (value == _isVisible) return;
            _isVisible = value;
            UpdateWindowForStateAndVisibility();
        }
    }

    public bool Exists { get; private set; }

    public unsafe bool IsExiting => Glfw.WindowShouldClose(WindowPtr);

    public unsafe WindowState WindowState
    {
        get => _windowState;
        init
        {
            if (_windowState == value) return;
            if (value != WindowState.Minimized) _notMinimizedWindowState = value;

            var shouldCacheSizeAndLocation = _windowState != WindowState.FullScreen &&
                                             _windowState != WindowState.Minimized &&
                                             value is WindowState.FullScreen or WindowState.Minimized;

            if (_windowState == WindowState.FullScreen && value != WindowState.FullScreen && _isVisible)
            {
                Glfw.SetWindowMonitor(WindowPtr, null, (int)_cachedWindowLocation.X,
                    (int)_cachedWindowLocation.Y,
                    (int)_cachedWindowClientSize.X,
                    (int)_cachedWindowClientSize.Y, 0);
            }

            if (shouldCacheSizeAndLocation)
            {
                _cachedWindowClientSize = ClientSize;
                _cachedWindowLocation = Location;
            }

            _windowState = value;

            UpdateWindowForStateAndVisibility();
        }
    }

    public unsafe WindowBorder WindowBorder
    {
        get => _windowBorder;
        set
        {
            Glfw.GetVersion(out var major, out var minor, out _);
            if (major == 3 && minor < 3)
                throw new NotSupportedException("Cannot be implemented in GLFW 3.2.");

            switch (value)
            {
                case WindowBorder.Hidden:
                    Glfw.SetWindowAttrib(WindowPtr, WindowAttribute.Decorated, false);
                    break;
                case WindowBorder.Resizable:
                    Glfw.SetWindowAttrib(WindowPtr, WindowAttribute.Decorated, true);
                    Glfw.SetWindowAttrib(WindowPtr, WindowAttribute.Resizable, true);
                    break;
                case WindowBorder.Fixed:
                    Glfw.SetWindowAttrib(WindowPtr, WindowAttribute.Decorated, true);
                    Glfw.SetWindowAttrib(WindowPtr, WindowAttribute.Resizable, false);
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(value), value, null);
            }

            _windowBorder = value;
        }
    }

    public unsafe Box2 Bounds
    {
        get => new(Location, Location + Size);
        set
        {
            Glfw.SetWindowSize(WindowPtr, (int)value.Size.X, (int)value.Size.Y);
            Glfw.SetWindowPos(WindowPtr, (int)value.Min.X, (int)value.Min.Y);
        }
    }

    public unsafe Vector2 Location
    {
        get => _location;
        set
        {
            Glfw.SetWindowPos(WindowPtr, (int)value.X, (int)value.Y);
            _location = value;
        }
    }

    public unsafe Vector2 Size
    {
        get => _size;
        set
        {
            _size = value;
            Glfw.SetWindowSize(WindowPtr, (int)value.X, (int)value.Y);
        }
    }

    public unsafe Vector2? MinimumSize
    {
        get => _minimumSize;
        set
        {
            _minimumSize = value;
            Glfw.SetWindowSizeLimits(WindowPtr, (int)(value?.X ?? Glfw.DontCare),
                (int)(value?.Y ?? Glfw.DontCare),
                (int)(_maximumSize?.X ?? Glfw.DontCare),
                (int)(_maximumSize?.Y ?? Glfw.DontCare));
        }
    }

    public unsafe Vector2? MaximumSize
    {
        get => _maximumSize;
        set
        {
            _maximumSize = value;
            Glfw.SetWindowSizeLimits(WindowPtr, (int)(_minimumSize?.X ?? Glfw.DontCare),
                (int)(_minimumSize?.Y ?? Glfw.DontCare),
                (int)(value?.X ?? Glfw.DontCare),
                (int)(value?.Y ?? Glfw.DontCare));
        }
    }

    public unsafe (int numerator, int denominator)? AspectRatio
    {
        get => _aspectRatio;
        set
        {
            _aspectRatio = value;
            Glfw.SetWindowAspectRatio(WindowPtr, value?.numerator ?? Glfw.DontCare,
                value?.denominator ?? Glfw.DontCare);
        }
    }

    public Box2 ClientRectangle
    {
        get => new(Location, Location + Size);
        set
        {
            Location = value.Min;
            Size = value.Size;
        }
    }

    public Vector2 ClientSize { get; private set; }

    public bool IsFullscreen => WindowState == WindowState.FullScreen;

    public unsafe MouseCursor Cursor
    {
        get => _managedCursor;
        set
        {
            _managedCursor = value ?? throw new ArgumentNullException(nameof(value),
                "Cursor cannot be null. To reset to default cursor, set it to MouseCursor.Default instead.");
            var oldCursor = _glfwCursor;
            _glfwCursor = null;

            if (value.Shape == StandardShape.CustomShape)
            {
                fixed (byte* ptr = value.Data)
                {
                    var cursorImg = new GraphicsLibraryFramework.Image(value.Width, value.Height, ptr);
                    _glfwCursor = Glfw.CreateCursor(cursorImg, value.X, value.Y);
                }
            }

            else if (value != MouseCursor.Default)
                _glfwCursor = Glfw.CreateStandardCursor(MapStandardCursorShape(value.Shape));

            Glfw.SetCursor(WindowPtr, _glfwCursor);

            if (oldCursor != null) Glfw.DestroyCursor(oldCursor);
        }
    }

    public unsafe CursorState CursorState
    {
        get
        {
            var inputMode = Glfw.GetInputMode(WindowPtr, CursorStateAttribute.Cursor);
            return inputMode switch
            {
                CursorModeValue.CursorNormal => CursorState.Normal,
                CursorModeValue.CursorHidden => CursorState.Hidden,
                CursorModeValue.CursorDisabled => CursorState.Grabbed,
                _ => throw new ArgumentOutOfRangeException()
            };
        }

        set
        {
            var inputMode = value switch
            {
                CursorState.Normal => CursorModeValue.CursorNormal,
                CursorState.Hidden => CursorModeValue.CursorHidden,
                CursorState.Grabbed => CursorModeValue.CursorDisabled,
                _ => throw new ArgumentOutOfRangeException()
            };

            Glfw.SetInputMode(WindowPtr, CursorStateAttribute.Cursor, inputMode);
        }
    }

    [Obsolete("Use CursorState insatead.")]
    public unsafe bool CursorVisible
    {
        get
        {
            var inputMode = Glfw.GetInputMode(WindowPtr, CursorStateAttribute.Cursor);
            return inputMode != CursorModeValue.CursorHidden && inputMode != CursorModeValue.CursorDisabled;
        }

        set => Glfw.SetInputMode(WindowPtr, CursorStateAttribute.Cursor,
            value ? CursorModeValue.CursorNormal : CursorModeValue.CursorHidden);
    }

    [Obsolete("Use CursorState instead.")]
    public unsafe bool CursorGrabbed
    {
        get => Glfw.GetInputMode(WindowPtr, CursorStateAttribute.Cursor) == CursorModeValue.CursorDisabled;
        set
        {
            if (value)
                Glfw.SetInputMode(WindowPtr, CursorStateAttribute.Cursor, CursorModeValue.CursorDisabled);
            else if (CursorVisible)
                Glfw.SetInputMode(WindowPtr, CursorStateAttribute.Cursor, CursorModeValue.CursorNormal);
            else
                Glfw.SetInputMode(WindowPtr, CursorStateAttribute.Cursor, CursorModeValue.CursorHidden);
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public unsafe void Focus()
    {
        Glfw.FocusWindow(WindowPtr);
    }

    private static void InitializeGlBindings()
    {
        Assembly assembly;
        try
        {
            assembly = Assembly.Load("OnDash.Graphics");
        }
        catch
        {
            return;
        }

        var provider = new GlfwBindingsContext();

        LoadBindings("ES11");
        LoadBindings("ES20");
        LoadBindings("ES30");
        LoadBindings("OpenGL");
        LoadBindings("OpenGL4");
        return;

        void LoadBindings(string typeNamespace)
        {
            var type = assembly.GetType($"OnDash.Graphics.{typeNamespace}.GL");
            if (type == null)
            {
                return;
            }

            var load = type.GetMethod("LoadBindings");
            load!.Invoke(null, new object[] { provider });
        }
    }

    private unsafe void HandleResize(int width, int height)
    {
        _size.X = width;
        _size.Y = height;

        Glfw.GetFramebufferSize(WindowPtr, out width, out height);

        ClientSize = new Vector2(width, height);
    }

    private unsafe void UpdateWindowForStateAndVisibility()
    {
        var isMsWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;

        if (!_isVisible)
        {
            Glfw.HideWindow(WindowPtr);
            return;
        }

        if (!isMsWindows)
        {
            Glfw.ShowWindow(WindowPtr);
        }

        switch (_windowState)
        {
            case WindowState.Normal:
                Glfw.RestoreWindow(WindowPtr);
                break;

            case WindowState.Minimized:
                Glfw.IconifyWindow(WindowPtr);
                break;

            case WindowState.Maximized:
                if (isMsWindows)
                    Glfw.RestoreWindow(WindowPtr);

                Glfw.MaximizeWindow(WindowPtr);
                break;

            case WindowState.FullScreen:
                if (isMsWindows)
                {
                    Glfw.ShowWindow(WindowPtr);
                    Glfw.RestoreWindow(WindowPtr);
                }

                var monitor = CurrentMonitor.ToUnsafePtr<GraphicsLibraryFramework.Monitor>();
                var modePtr = Glfw.GetVideoMode(monitor);
                Glfw.SetWindowMonitor(WindowPtr, monitor, 0, 0, modePtr->Width, modePtr->Height, modePtr->RefreshRate);
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

    private unsafe WindowState GetWindowStateFromGlfw()
    {
        if (Glfw.GetWindowAttrib(WindowPtr, WindowAttributeGetBool.Iconified))
            return WindowState.Minimized;

        if (Glfw.GetWindowAttrib(WindowPtr, WindowAttributeGetBool.Maximized))
            return WindowState.Maximized;

        return Glfw.GetWindowMonitor(WindowPtr) != null ? WindowState.FullScreen : WindowState.Normal;
    }

    private unsafe void RegisterWindowCallbacks()
    {
        _windowPosCallback = WindowPosCallback;
        _windowSizeCallback = WindowSizeCallback;
        _windowCloseCallback = WindowCloseCallback;
        _windowRefreshCallback = WindowRefreshCallback;
        _windowFocusCallback = WindowFocusCallback;
        _windowIconifyCallback = WindowIconifyCallback;
        _windowMaximizeCallback = WindowMaximizeCallback;
        _mouseButtonCallback = MouseButtonCallback;
        _cursorPosCallback = CursorPosCallback;
        _cursorEnterCallback = CursorEnterCallback;
        _scrollCallback = ScrollCallback;
        _keyCallback = KeyCallback;
        _charCallback = CharCallback;
        _dropCallback = DropCallback;
        _joystickCallback = JoystickCallback;

        Glfw.SetWindowPosCallback(WindowPtr, _windowPosCallback);
        Glfw.SetWindowSizeCallback(WindowPtr, _windowSizeCallback);
        Glfw.SetWindowCloseCallback(WindowPtr, _windowCloseCallback);
        Glfw.SetWindowRefreshCallback(WindowPtr, _windowRefreshCallback);
        Glfw.SetWindowFocusCallback(WindowPtr, _windowFocusCallback);
        Glfw.SetWindowIconifyCallback(WindowPtr, _windowIconifyCallback);
        Glfw.SetWindowMaximizeCallback(WindowPtr, _windowMaximizeCallback);

        Glfw.SetMouseButtonCallback(WindowPtr, _mouseButtonCallback);
        Glfw.SetCursorPosCallback(WindowPtr, _cursorPosCallback);
        Glfw.SetCursorEnterCallback(WindowPtr, _cursorEnterCallback);
        Glfw.SetScrollCallback(WindowPtr, _scrollCallback);

        Glfw.SetKeyCallback(WindowPtr, _keyCallback);
        Glfw.SetCharCallback(WindowPtr, _charCallback);

        Glfw.SetDropCallback(WindowPtr, _dropCallback);

        Joysticks.JoystickCallback += _joystickCallback;
    }

    private void UnregisterWindowCallbacks()
    {
        Joysticks.JoystickCallback -= _joystickCallback;
    }

    private unsafe void InitialiseJoystickStates()
    {
        for (var i = 0; i < _joystickStates.Length; i++)
        {
            if (!Glfw.JoystickPresent(i)) continue;
            Glfw.GetJoystickHatsRaw(i, out var hatCount);
            Glfw.GetJoystickAxesRaw(i, out var axisCount);
            Glfw.GetJoystickButtonsRaw(i, out var buttonCount);
            var name = Glfw.GetJoystickName(i);

            _joystickStates[i] = new JoystickState(hatCount, axisCount, buttonCount, i, name);
        }
    }

    private unsafe void WindowPosCallback(Window* window, int x, int y)
    {
        try
        {
            OnMove(new WindowPositionEventArgs(x, y));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void WindowSizeCallback(Window* window, int width, int height)
    {
        try
        {
            OnResize(new ResizeEventArgs(width, height));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void WindowCloseCallback(Window* window)
    {
        try
        {
            var c = new CancelEventArgs();
            OnClosing(c);
            if (c.Cancel) Glfw.SetWindowShouldClose(WindowPtr, false);
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void WindowRefreshCallback(Window* window)
    {
        try
        {
            OnRefresh();
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void WindowFocusCallback(Window* window, bool focused)
    {
        try
        {
            OnFocusedChanged(new FocusedChangedEventArgs(focused));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void WindowIconifyCallback(Window* window, bool iconified)
    {
        try
        {
            OnMinimized(new MinimizedEventArgs(iconified));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void WindowMaximizeCallback(Window* window, bool maximized)
    {
        try
        {
            OnMaximized(new MaximizedEventArgs(maximized));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void MouseButtonCallback(Window* window, MouseButton button, InputAction action, KeyModifiers mods)
    {
        try
        {
            var args = new MouseButtonEventArgs(button, action, mods);

            if (action == InputAction.Release)
            {
                MouseState[button] = false;
                OnMouseUp(args);
            }
            else
            {
                MouseState[button] = true;
                OnMouseDown(args);
            }
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void CursorPosCallback(Window* window, float posX, float posY)
    {
        try
        {
            var newPos = new Vector2(posX, posY);
            var delta = newPos - _lastReportedMousePos;

            _lastReportedMousePos = newPos;

            OnMouseMove(new MouseMoveEventArgs(newPos, delta));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void CursorEnterCallback(Window* window, bool entered)
    {
        try
        {
            if (entered) OnMouseEnter();
            else OnMouseLeave();
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void ScrollCallback(Window* window, float offsetX, float offsetY)
    {
        try
        {
            var offset = new Vector2(offsetX, offsetY);
            MouseState.Scroll += offset;
            OnMouseWheel(new MouseWheelEventArgs(offset));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void KeyCallback(Window* window, Keys key, int scancode, InputAction action, KeyModifiers mods)
    {
        try
        {
            var args = new KeyboardKeyEventArgs(key, scancode, mods, action == InputAction.Repeat);

            if (action == InputAction.Release)
            {
                if (key != Keys.Unknown)
                    KeyboardState.SetKeyState(key, false);
                OnKeyUp(args);
            }
            else
            {
                if (key != Keys.Unknown)
                    KeyboardState.SetKeyState(key, true);
                OnKeyDown(args);
            }
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void CharCallback(Window* window, uint codepoint)
    {
        try
        {
            OnTextInput(new TextInputEventArgs((int)codepoint));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void DropCallback(Window* window, int count, byte** paths)
    {
        try
        {
            var arrayOfPaths = new string[count];

            for (var i = 0; i < count; i++)
                arrayOfPaths[i] = Utils.PtrToStringUtf8(paths[i]);

            OnFileDrop(new FileDropEventArgs(arrayOfPaths));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }

    private unsafe void JoystickCallback(int joy, ConnectedState eventCode)
    {
        try
        {
            if (eventCode == ConnectedState.Connected)
            {
                Glfw.GetJoystickHatsRaw(joy, out var hatCount);
                Glfw.GetJoystickAxesRaw(joy, out var axisCount);
                Glfw.GetJoystickButtonsRaw(joy, out var buttonCount);
                var name = Glfw.GetJoystickName(joy);

                _joystickStates[joy] = new JoystickState(hatCount, axisCount, buttonCount, joy, name);
            }
            else _joystickStates[joy] = null!;

            OnJoystickConnected(new JoystickEventArgs(joy, eventCode == ConnectedState.Connected));
        }
        catch (Exception e)
        {
            _callbackExceptions.Enqueue(ExceptionDispatchInfo.Capture(e));
        }
    }


    public unsafe void Close()
    {
        var c = new CancelEventArgs();
        OnClosing(c);
        if (c.Cancel == false)
            Glfw.SetWindowShouldClose(WindowPtr, true);
    }

    public void MakeCurrent()
    {
        if (Context == null)
            throw new InvalidOperationException("Cannot make a context current when running with ContextAPI.NoAPI.");

        Context?.MakeCurrent();
    }

    public bool ProcessEvents(double timeout)
    {
        Glfw.WaitEventsTimeout(timeout);

        ProcessInputEvents();
        RethrowCallbackExceptionsIfNeeded();

        return true;
    }

    [Obsolete(
        "This function wrongly implies that only events from this window are processed, while in fact events for all windows are processed. Use NativeWindow.ProcessWindowEvents() instead.")]
    public void ProcessEvents()
    {
        ProcessInputEvents();

        ProcessWindowEvents(IsEventDriven);
    }

    public static void ProcessWindowEvents(bool waitForEvents)
    {
        if (waitForEvents) Glfw.WaitEvents();
        else Glfw.PollEvents();

        RethrowCallbackExceptionsIfNeeded();
    }

    private static void RethrowCallbackExceptionsIfNeeded()
    {
        while (_callbackExceptions.TryDequeue(out var exception))
            _localThreadExceptions.Add(exception);

        switch (_localThreadExceptions.Count)
        {
            case 1:
            {
                var exception = _localThreadExceptions[0];
                _localThreadExceptions.Clear();
                exception!.Throw();
                break;
            }
            case > 1:
            {
                var exceptions = new Exception[_localThreadExceptions.Count];
                for (var i = 0; i < _localThreadExceptions.Count; i++)
                    exceptions[i] = _localThreadExceptions[i]!.SourceException;

                Exception exception =
                    new AggregateException("Multiple exceptions in callback handlers while processing events.",
                        exceptions);
                _localThreadExceptions.Clear();
                throw exception;
            }
        }
    }

    public unsafe void ProcessInputEvents()
    {
        MouseState.Update();
        KeyboardState.Update();

        Glfw.GetCursorPos(WindowPtr, out var x, out var y);
        MouseState.Position = new Vector2(x, y);

        foreach (var t in _joystickStates)
            t.Update();
    }

    public Vector2 PointToClient(Vector2 point) =>
        point - Location;

    public Vector2 PointToScreen(Vector2 point) =>
        point + Location;

    public event Action<WindowPositionEventArgs> Move;

    public event Action<ResizeEventArgs> Resize;

    public event Action Refresh;

    public event Action<CancelEventArgs> Closing;

    [Obsolete("This event will never be invoked.")]
    public event Action Closed;

    public event Action<MinimizedEventArgs> Minimized;
    public event Action<MaximizedEventArgs> Maximized;
    public event Action<JoystickEventArgs> JoystickConnected;
    public event Action<FocusedChangedEventArgs> FocusedChanged;
    public event Action<KeyboardKeyEventArgs> KeyDown;
    public event Action<TextInputEventArgs> TextInput;
    public event Action<KeyboardKeyEventArgs> KeyUp;

    [Obsolete("Use the Monitors.OnMonitorConnected event instead.", true)]
    public event Action<MonitorEventArgs> MonitorConnected;

    public event Action MouseLeave;
    public event Action MouseEnter;

    public event Action<MouseButtonEventArgs> MouseDown;
    public event Action<MouseButtonEventArgs> MouseUp;
    public event Action<MouseMoveEventArgs> MouseMove;
    public event Action<MouseWheelEventArgs> MouseWheel;
    public event Action<FileDropEventArgs> FileDrop;

    public bool IsKeyDown(Keys key) =>
        KeyboardState.IsKeyDown(key);

    public bool IsKeyPressed(Keys key) =>
        KeyboardState.IsKeyDown(key) && !KeyboardState.WasKeyDown(key);

    public bool IsKeyReleased(Keys key) =>
        !KeyboardState.IsKeyDown(key) && KeyboardState.WasKeyDown(key);

    public bool IsMouseButtonDown(MouseButton button) =>
        MouseState.IsButtonDown(button);

    public bool IsMouseButtonPressed(MouseButton button) =>
        MouseState.IsButtonDown(button) && !MouseState.WasButtonDown(button);

    public bool IsMouseButtonReleased(MouseButton button) =>
        !MouseState.IsButtonDown(button) && MouseState.WasButtonDown(button);

    [Obsolete("Use Monitors.GetMonitorFromWindow instead", true)]
    public unsafe MonitorInfo FindMonitor() =>
        Monitors.GetMonitorFromWindow(WindowPtr);

    public bool TryGetCurrentMonitorScale(out float horizontalScale, out float verticalScale)
    {
        var info = Monitors.GetMonitorFromWindow(this);
        horizontalScale = info.HorizontalScale;
        verticalScale = info.VerticalScale;
        return true;
    }

    public bool TryGetCurrentMonitorDpi(out float horizontalDpi, out float verticalDpi)
    {
        var info = Monitors.GetMonitorFromWindow(this);
        horizontalDpi = info.HorizontalDpi;
        verticalDpi = info.VerticalDpi;
        return true;
    }

    public bool TryGetCurrentMonitorDpiRaw(out float horizontalRawDpi, out float verticalRawDpi)
    {
        var info = Monitors.GetMonitorFromWindow(this);
        horizontalRawDpi = info.HorizontalRawDpi;
        verticalRawDpi = info.VerticalRawDpi;
        return true;
    }


    protected void OnMove(WindowPositionEventArgs e)
    {
        Move.Invoke(e);

        _location.X = e.X;
        _location.Y = e.Y;
    }

    protected void OnResize(ResizeEventArgs e)
    {
        HandleResize(e.Width, e.Height);
        Resize.Invoke(e);
    }

    protected void OnRefresh()
    {
        Refresh.Invoke();
    }

    protected void OnClosing(CancelEventArgs e)
    {
        Closing.Invoke(e);
    }

    [Obsolete("This method will never be called.")]
    protected void OnClosed()
    {
        Closed.Invoke();
    }

    protected void OnJoystickConnected(JoystickEventArgs e)
    {
        JoystickConnected.Invoke(e);
    }

    protected void OnFocusedChanged(FocusedChangedEventArgs e)
    {
        FocusedChanged.Invoke(e);

        _isFocused = e.IsFocused;
    }

    protected void OnKeyDown(KeyboardKeyEventArgs e)
    {
        KeyDown.Invoke(e);
    }

    protected void OnTextInput(TextInputEventArgs e)
    {
        TextInput.Invoke(e);
    }

    protected void OnKeyUp(KeyboardKeyEventArgs e)
    {
        KeyUp.Invoke(e);
    }

    [Obsolete("Use the Monitors.OnMonitorConnected event instead.", true)]
    protected void OnMonitorConnected(MonitorEventArgs e)
    {
        MonitorConnected.Invoke(e);
    }

    protected void OnMouseLeave()
    {
        MouseLeave.Invoke();
    }

    protected void OnMouseEnter()
    {
        MouseEnter.Invoke();
    }

    protected void OnMouseDown(MouseButtonEventArgs e)
    {
        MouseDown.Invoke(e);
    }

    protected void OnMouseUp(MouseButtonEventArgs e)
    {
        MouseUp.Invoke(e);
    }

    protected void OnMouseMove(MouseMoveEventArgs e)
    {
        MouseMove.Invoke(e);
    }

    protected void OnMouseWheel(MouseWheelEventArgs e)
    {
        MouseWheel.Invoke(e);
    }

    protected void OnMinimized(MinimizedEventArgs e)
    {
        _windowState = e.IsMinimized ? WindowState.Minimized : GetWindowStateFromGlfw();
        Minimized.Invoke(e);
    }

    protected void OnMaximized(MaximizedEventArgs e)
    {
        _windowState = e.IsMaximized ? WindowState.Maximized : GetWindowStateFromGlfw();
        if (_windowState != WindowState.Minimized) _notMinimizedWindowState = _windowState;

        Maximized.Invoke(e);
    }

    protected void OnFileDrop(FileDropEventArgs e)
    {
        FileDrop.Invoke(e);
    }

    protected unsafe void Dispose(bool disposing)
    {
        if (_disposedValue) return;

        if (GlfwProvider.IsOnMainThread)
        {
            UnregisterWindowCallbacks();
            Glfw.DestroyWindow(WindowPtr);
            Exists = false;
        }
        else
            throw new GlfwException(
                "You can only dispose windows on the main thread. The window needs to be disposed as it cannot safely be disposed in the finalizer.");

        _disposedValue = true;
    }

    ~NativeWindow()
    {
        Dispose(false);
    }

    private static CursorShape MapStandardCursorShape(StandardShape shape)
    {
        return shape switch
        {
            StandardShape.Arrow => CursorShape.Arrow,
            StandardShape.IBeam => CursorShape.IBeam,
            StandardShape.Crosshair => CursorShape.Crosshair,
            StandardShape.Hand => CursorShape.Hand,
            StandardShape.HResize => CursorShape.HResize,
            StandardShape.VResize => CursorShape.VResize,
            StandardShape.CustomShape => CursorShape.Custom,
            _ => throw new ArgumentOutOfRangeException(nameof(shape), shape, null)
        };
    }

    public void CenterWindow() =>
        CenterWindow(Size);

    public void CenterWindow(Vector2 newSize)
    {
        var monitorInfo = Monitors.GetMonitorFromWindow(this);

        var monitorRectangle = monitorInfo.ClientArea;
        var x = (monitorRectangle.Min.X + monitorRectangle.Max.X - newSize.X) * 0.5f;
        var y = (monitorRectangle.Min.Y + monitorRectangle.Max.Y - newSize.Y) * 0.5f;

        if (x < monitorRectangle.Min.X) x = monitorRectangle.Min.X;

        if (y < monitorRectangle.Min.Y) y = monitorRectangle.Min.Y;
        ClientRectangle = new Box2(x, y, x + newSize.X, y + newSize.Y);
    }
}